You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implements the kbagent side of RLS v2 (row-level security), companion to keboola-mcp-server PR #709, which redesigns RLS around metastore-backed rls-policy / cls-policy objects (declarative condition primitives instead of hand-written SQL). kbagent only authors the policies; enforcement is query_data in keboola-mcp-server.
New rls command group: list, detail, schema, create, update, delete, and a guided setup wizard (checkbox table picker + interactive condition builder, interactive-terminal-only).
New cls command group (column-level security): list, detail, schema, create, update, delete. Each rule is {principal|principals, visible_columns} -- an allowlist projection. ClsService subclasses RlsService; no setup wizard.
Scope:targeted by default (the metastore schema's own default: the owning project plus --target-project grants). --scope organization governs the table in every project of the organization; it is explicit and destructive-class (rls|cls.create --scope organization in FLAG_ESCALATIONS, also checked over REST). There is no project scope (the schema does not support it). --target-project takes an alias or a project ID (the feat(semantic-layer): metastore scope/ACL support (targeted + org-wide) [AI-3790] #715 resolver).
Who may write (metastore schema ACL): a project admin may create/update/delete targeted policies of its own project without grants; grants and organization scope need the organization-admin role. Policies are enforced only where the project has the row-level-security feature.
Policies that would block every query are refused before any write. The enforcement refuses reads when a policy's dialect differs from the project backend (--dialect defaults to the backend), and kbagent refuses that mismatch locally. A null comparison (renders col = NULL, matches nothing) and principals with whitespace are refused too.
Schema 1.1.0 (go-monorepo keboola/go-monorepo#617, AI-4010; the metastore default once it ships): rules select identities by principal, principals or IdP groups; every rule matching one identity applies (RLS conditions OR, CLS columns union), so the old duplicate-principal refusal is gone; rls --default (CLI and REST) sets the condition for an identified reader no rule matches, and {"false": true} means no rows; value {"$identity": "email"} / values {"$identity": "groups"} placeholders are accepted and previewed. A default cannot be removed by update (recreate the policy).
update is a partial PATCH (new MetastoreClient.patch_item) of only the changed keys. The policy is read first and the MERGED result is validated, because the metastore PATCH validates only the keys it receives; grants change before the rules; the result is re-read after the write. The scope support in metastore_client.py (post_item scope, put_target_projects) comes from feat(semantic-layer): metastore scope/ACL support (targeted + org-wide) [AI-3790] #715.
--dry-run preview follows the enforcement's rendering (columns quoted per dialect, TRUE/FALSE), but is not the enforcement.
Permissions: create/update/setup are admin; delete is destructive (and has --dry-run).
REST: /rls and /cls routers built from one builder, every route permission-gated: POST /{group}/{project} (table_id, rules, dialect?, scope, target_projects), PATCH /{group}/{project}/{policy_id}, DELETE ...?dry_run=true.
Full CONTRIBUTING.md doc tax: CLAUDE.md, context.pyAGENT_CONTEXT, commands-reference.md, gotchas.md, keboola-expert.md, SKILL.md (regenerated), new rls-workflow.md, docs/error-codes.md, docs/web-server-endpoints.md (regenerated).
Backend availability: the rls-policy and cls-policy object types are registered by the metastore's 2026-09-29 schema migrations (go-monorepo, released as metastore-v0.11.0). A stack whose metastore predates them answers rls schema / cls schema with a classified NOT_FOUND (see gotchas.md).
Known enforcement-side hazard (for #709): an organization policy is loaded by every project in the organization, whatever its backend, so in a mixed Snowflake/BigQuery organization it blocks every project on the other backend. kbagent can only check the owner's backend; the docs recommend targeted there.
No version bump, no changelog.py entry -- per this repo's binding convention, feature PRs never bump the version; (since vNEXT) placeholders are resolved by the release PR.
Test Plan
tests/test_rls_service.py, tests/test_cls_service.py (scope default and combinations, backend dialect, schema 1.1.0 (groups, OR-combined rules, default, $identity), PATCH of changed keys, grants-before-rules, re-read, 409 message, delete dry-run, preview rendering per dialect).
tests/test_rls_cli.py, tests/test_cls_cli.py (flags, --scope organization and delete blocked by --deny-destructive, setup --json JSON error envelope).
tests/test_e2e.py: authors a targeted policy with no grants and the project's own dialect, updates its rules, delete --dry-run, deletes it (not run locally: needs E2E_API_TOKEN).
Full suite after the rebase onto 0.97.0: 7686 passed, 66 skipped. ruff check, ruff format --check, ty check, loc-check, check-sentinel-guards, check_command_sync.py, check_version_gates.py, check_error_codes.py, skill-frontmatter length and the SKILL.md / web-server-endpoints.md generators all pass.
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Local validation/preview currently treats any {"true": ...} as always-true (even when the value is falsey), which can silently accept and display an invalid condition shape when the live schema is unavailable.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds first-class Row-Level Security (RLS v2) support to kbagent by introducing a new rls command group and a REST router backed by a metastore rls-policy object type, including validation + preview tooling and the required permissions/doc/test integration across the CLI, server, and plugin skill surfaces.
Changes:
Implemented kbagent rls {list,detail,schema,create,update,delete,setup} with RlsService orchestration and local condition/rules validation + preview rendering.
Extended metastore client write envelopes to support organization / targeted scopes and added a target-projects grant management call for targeted RLS policies.
Added /rls server router with permission gating, updated operation registry classifications, and refreshed docs/plugin skill references + test coverage.
File summaries
File
Description
tests/test_server_rls.py
Verifies /rls router → service kwarg parity and permission gating behavior.
tests/test_rls_service.py
Unit-tests RlsService CRUD orchestration, scope handling, validation, and preview rendering.
tests/test_rls_cli.py
CLI-level tests for kbagent rls JSON/human output and exit-code/error mapping (incl. non-TTY setup refusal).
tests/test_metastore_client.py
Adds coverage for new metastore scope envelope fields and put_target_projects().
tests/test_e2e.py
Adds an E2E “clean failure / skip” check for rls schema and rls list while backend type is unregistered.
src/keboola_agent_cli/services/rls_service.py
New service composing metastore primitives into rls-policy CRUD + preview/dry-run behavior.
src/keboola_agent_cli/services/_rls_condition.py
New pure helpers for local rule validation, op validation, Draft7 schema validation, and condition preview rendering.
src/keboola_agent_cli/server/routers/rls.py
New FastAPI router exposing REST equivalents for rls commands (except interactive setup).
src/keboola_agent_cli/server/dependencies.py
Wires RlsService into ServiceRegistry.
src/keboola_agent_cli/server/app.py
Registers the rls router and documents it in the server router list.
compile_condition_preview() currently returns "TRUE" whenever the key 'true' is present, regardless of its value. Even with improved validation, this function can be called independently (e.g., future reuse) and should not render {"true": false} as an always-true predicate.
Preserve existing trigger phrases when adding new ones
plugins/kbagent/skills/kbagent/SKILL.md:21
This replacement removes the existing trigger phrases zero-copy clone and workspace load type while adding rls, cls. That narrows activation for already-supported workspace workflows; append the new triggers instead of dropping the old ones.
Fail on table-listing errors instead of reporting no tables
src/keboola_agent_cli/commands/rls.py:606
StorageService.list_tables() returns per-project failures in tables_result["errors"] instead of raising. This path ignores that field and reports No tables found with exit 0 when table listing failed (for example, an invalid token), so rls setup can appear successful without creating anything. Surface the errors and fail before treating the list as empty.
Validate policy operation type before set membership check
op comes directly from arbitrary JSON, so a malformed rule such as {"column": "x", "op": []} makes op not in RLS_CONDITION_OPS raise TypeError because lists are unhashable. With the live schema unavailable, this escapes the CLI's handled exceptions as a traceback instead of INVALID_RLS_POLICY; guard that op is a string before the set-membership check.
The CLS REST models have the same list[str] contract, so numeric project IDs in a JSON request are rejected before reaching the shared project_ids_as_ints validation. This makes the REST API inconsistent with the metastore wire contract and the existing auth/sharing project-ID models; use an integer-compatible input type for both CLS request models.
The REST body declares target project IDs as strings, but the metastore contract implemented in this PR serializes targetProjectIds as positive integers (and the other REST models use list[int]). A normal REST request such as {"target_project_ids": [999]} is therefore rejected by Pydantic with 422 before the service can normalize it; accept integer IDs (or both integer/string inputs) here and in the update model.
Validate local principal types and non-empty values
The local principal check uses truthiness and only checks that principals is a list. When live-schema validation is unavailable (a documented degraded path), values such as {"principal": {"email": "a@x.com"}} or {"principals": [123]} pass and are sent as policy rules even though the enforcement contract requires principal strings. Validate the selected field's type and non-empty string members locally, while still rejecting both fields being present.
The REST body models target project IDs as strings, but these are numeric project IDs on the wire (targetProjectIds: [int]) and the existing /sharing REST model uses list[int] for the same concept. A normal JSON request such as {"target_project_ids": [123]} can therefore be rejected by Pydantic before reaching the service. Use a numeric (or explicitly int-or-string) model for both create and update bodies, then normalize before calling the service.
The REST body models target project IDs as strings, but these are numeric project IDs on the wire (targetProjectIds: [int]) and the existing /sharing REST model uses list[int] for the same concept. A normal JSON request such as {"target_project_ids": [123]} can therefore be rejected by Pydantic before reaching the service. Use a numeric (or explicitly int-or-string) model for both create and update bodies, then normalize before calling the service.
This new helper returns two semantically distinct values as a positional tuple, which violates the repository's binding convention that multi-value returns use a dataclass. A named result would make the errors/warnings contract explicit and avoid positional coupling as validation grows.
op can come from arbitrary JSON, including an array or object. Testing an unhashable value with op not in RLS_CONDITION_OPS raises TypeError, so malformed rules can escape the intended INVALID_RLS_POLICY validation and produce a traceback (and rls setup does not catch TypeError). Check that op is a string before membership testing.
Preserve existing skill trigger phrases
plugins/kbagent/skills/kbagent/SKILL.md:21
This replacement drops the existing zero-copy clone and workspace load type trigger phrases from the skill description while adding rls, cls. That weakens the skill's activation for those already-supported workflows; retain the old triggers and append the new ones instead.
The update preserves whatever meta["scope"] contains, including "project", so an externally-created or legacy project-scoped policy can be modified through this command even though the service contract says RLS/CLS writes must never use project scope. Reject an existing unsupported scope before issuing put_item (defaulting only when the metadata is absent).
test_rls_service.py, test_rls_cli.py, test_server_rls.py (new) cover
scope resolution, fetch-then-merge updates, condition compilation,
permission gating (cli:admin blocks writes, reads stay reachable), and
router-to-service kwarg parity, all against a mocked MetastoreClient.
test_metastore_client.py extended for the new scope/target_project_ids
params and put_target_projects(). test_e2e.py adds an rls schema/list
case that expects a clean, classified failure -- the rls-policy object
type isn't registered on any real metastore backend yet (companion
go-monorepo work, tracked separately).
CLAUDE.md '## All CLI Commands', commands/context.py AGENT_CONTEXT,
commands-reference.md, and keboola-expert.md's tool-selection matrix
per CONTRIBUTING's "Checklist: Adding a New CLI Command". New
rls-workflow.md (guided setup, scripted creation, condition primitive
cookbook, targeted-scope sharing, troubleshooting the not-yet-available
backend). gotchas.md gains the RLS entry (since vNEXT). SKILL.md
frontmatter triggers + workflow table + regenerated auto-command-table
(trimmed elsewhere to stay under the 1024-char Claude Desktop limit).
docs/error-codes.md documents INVALID_RLS_POLICY; web-server-endpoints.md
regenerated for the new /rls router.
No version bump, no changelog.py entry -- feature PRs never bump the
version per this repo's binding convention; the release PR resolves
the (since vNEXT) placeholder.
… policies
Adds list/detail/schema/create/update/delete for the metastore cls-policy
object type, the column-level sibling of rls-policy: each rule names a
principal and a visible_columns allowlist. Same scope rule as rls
(organization/targeted, never project), fetch-then-merge update, admin
permission class for writes and a /cls REST mirror. No setup wizard.
- ClsService subclasses RlsService; item type, label, error code and the two
rule hooks (local validation, dry-run preview) are the only differences.
Shared principal check moves to _rls_condition.validate_principal_fields.
- New error code INVALID_CLS_POLICY; cls-policy added to MetastoreClient's
item-type literal.
- Tests: service, CLI and server suites for cls; E2E schema/list case covers
both rls and cls.
- Docs: every hand-maintained surface (CLAUDE.md, AGENT_CONTEXT, commands
reference, gotchas, rls-workflow, expert prompt, error codes) plus the
regenerated SKILL.md and web-server-endpoints.md.
- Correct stale RLS claims: the rls-policy/cls-policy schemas now exist in the
metastore (a stack with an older metastore answers a classified NOT_FOUND),
and the ACL reserves organization scope and cross-project grants for
organization admins while the owning project's admin may author targeted
policies.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…d validation
The metastore's bare /api/v1/schema/<type> endpoint returns only a {"versions": [...]}
listing; the JSON Schema itself lives at /<version>. rls/cls schema therefore printed
version metadata, and the live structural validation in create/update checked nothing
(a listing has no constraints, so any body passed).
Move the existing default-version resolution of SemanticLayerService into a shared
fetch_resolved_schema() in metastore_client.py and use it from the semantic-layer,
rls and cls services.
Tests: resolver unit tests (default version, first-version fallback, pass-through) and
a cls regression test that validation really runs against the resolved schema.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- rls setup: validate --dialect before the wizard; require an interactive stdin AND stdout
before any API call; exit non-zero (and name the tables) when any policy could not be
created instead of exiting 0.
- validation: the true sentinel must be exactly {"true": true}; the shape of every
condition primitive (column, value, values) is checked locally, so a malformed rule
fails as INVALID_RLS_POLICY even when the live schema is unavailable instead of
crashing the preview.
- schema fetch: an authentication/permission failure (INVALID_TOKEN, MISSING_MASTER_TOKEN,
access denied, 401/403) is raised instead of being reported as a missing schema; only an
unavailable schema degrades. rls/cls schema report it with the right exit code.
- create/update add a "warnings" entry (printed in human mode) when live structural
validation was skipped.
- update: --clear-target-projects revokes every grant (mutually exclusive with
--target-project); the policy keeps its current scope.
- SEMANTIC_TYPES includes cls-policy; the E2E schema skip now asserts NOT_FOUND.
- Docs: new flag, gotchas, rls-workflow wording.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ddress review
Found while answering a Copilot comment on a stale docstring: the metastore decodes
targetProjectIds as []int, and PUT .../target-projects takes {"targetProjectIds": [int]}
(an empty list clears every grant). The client sent string ids in the create/update
envelope (a 400) and {"projectIds": [...]} to the grants endpoint -- a field the backend
ignores, so --target-project would have cleared the grants instead of setting them. It also
parsed the 204 No Content reply as JSON.
- Convert target project ids to positive integers (INVALID_ARGUMENT before any request
otherwise); send the right field; treat 204 as an empty result.
- Replace the "speculative" docstring with the deployed contract.
- fetch_resolved_schema returns a named ResolvedSchema (schema, version) instead of a tuple.
- A non-empty but malformed schema from the metastore degrades to a warning instead of raising
SchemaError from the structural validation.
- E2E: rls/cls list may only be skipped on NOT_FOUND, like schema.
- Docs: fix the contradictory cli:write / cli:admin wording; note numeric --target-project ids.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…t ids in --dry-run
- update: only an explicit, non-empty --target-project list derives a new scope. When the
option is omitted or the grants are being cleared, the policy keeps its current scope. A
targeted policy whose grants were cleared is legitimately "targeted" with an empty list;
an unrelated update used to turn it into "organization".
- create/update: validate the target project ids before any network call, so --dry-run runs
the same validation as the real write (INVALID_ARGUMENT) instead of reporting a successful
preview for an id the write would reject.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… keys (Copilot review)
validate_condition_ops handled `{"and": [...], "or": [...]}` by validating only the "and" branch
and returning, so with the live schema unavailable the policy no longer matched the submitted
tree. An and/or condition must now have exactly one of those keys and no others, and a leaf
condition may carry only its op's own keys (value for comparisons, values for in/not_in), as the
schema's additionalProperties:false requires. compile_condition_preview is equally strict.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ved version listing (Copilot review)
- validate_principal_fields decided "exactly one of principal/principals" by truthiness, so
{"principal": "a", "principals": []} or {"principal": 1} passed when the live schema was
unavailable and the service wrote a rule that does not satisfy the documented shape. Key
presence now decides "exactly one", and the value is checked: principal must be a non-empty
string, principals a non-empty list of non-empty strings. (Shared with the cls service.)
- fetch_resolved_schema returns the bare {"versions": [...]} listing when it cannot pick a
version. That is a valid, constraint-free JSON Schema, so validating against it silently
skipped every structural check without the documented warning. It is now treated as
unavailable (schema=None with a reason, surfaced as a warning on create/update).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…pdate re-sends no grants; non-string op (Copilot review)
- `rls setup` performs `create`'s writes but was only gated as `rls.setup`; an exact `rls.create`
denial now blocks it too (checked before any API call).
- When every table of a `setup` fails, the exit code now follows the error class of the failures
(auth -> 3, config -> 5, ...) like `rls create`, instead of a synthetic general failure; a mix of
classes stays 1.
- `update` omitting --target-project re-sent the grant snapshot it had just read through
put_target_projects, which could overwrite a concurrent grant change. Only an explicit list (or
the clear path) touches grants now.
- validate_condition_ops tested `op in <set>` with the raw value, so an unhashable op such as `[]`
raised TypeError. A non-string op is reported as an unknown op.
- E2E: create/detail/update/delete round-trip for rls and cls (skips on a stack or token that cannot
author policies; the policy is always deleted) and the `rls setup` refusal under --json.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… error code in the setup envelope (Copilot review)
- project_ids_as_ints used int(), which truncates 1.5 to 1 and turns True into 1, so a REST or
programmatic caller could grant a policy to a different project than the one named. Booleans and
non-integral floats are rejected (2.0 is still accepted as 2).
- When every table of `rls setup` fails with the same error class, the final error now carries that
class's code (INVALID_TOKEN, CONFIG_ERROR, ...) instead of a fixed API_ERROR, matching its exit
code; a mix of classes stays API_ERROR.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The services took `list[int | str]`, which `list[int]` from the REST bodies is not assignable to
(lists are invariant). They now take `Sequence[int | str]`, and project_ids_as_ints takes a
Sequence. The test helper's id list is typed loosely. No behaviour change.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ect ids in REST
Replaces the merge commit a121636 that the rebase onto main flattened:
the rls/cls services use the scope client #715 shipped on main, and the
REST bodies accept integer project ids (Copilot review).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PATCH merges only the given top-level data keys under a row lock
(go-monorepo simpleJSONMerge), so unknown keys and concurrent changes
to other keys survive a policy update. The server validates only the
patched keys, so callers validate the merged record themselves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… enforcement
The enforcement refuses all queries of a project when one policy names
a principal twice on a table, or has a principal with whitespace; a null
comparison renders col = NULL and matches nothing. Refuse all three
locally. The preview quotes columns per dialect and renders booleans as
TRUE/FALSE, as the enforcement does. Target project 0 is rejected (the
metastore requires positive IDs).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, destructive deletes
Address the review of #778:
- default to targeted scope (the schema default); --scope organization
is explicit and destructive-class (FLAG_ESCALATIONS, also over REST)
- --dialect defaults to and must match the project backend; duplicate
principals are refused across the visible policies on the table
- --target-project takes alias or ID (#715 resolver); grants go in the
create request only; on update they change before the rules
- update sends a PATCH of the changed keys and re-reads the result
- rls/cls delete are destructive and have --dry-run
- own 409 message, owner_project_id in list/detail
- command shape per #791: --table-id, --dialect choice, REST table_id /
target_projects and PATCH; one router builder for rls and cls
- rls setup --json returns a JSON error envelope; typed wizard values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every listed policy applies to the project; organization scope reaches
every project of the organization. Document the schema ACL (project
admin vs organization admin), the row-level-security feature, the
checks that keep one policy from blocking all queries, and the new
flags. Restore the SKILL.md trigger keywords; regenerate the endpoint
reference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An organization-scope policy in the shared E2E organization would reach
every project there, and switching its dialect would block their
queries. Use the default targeted scope and the project's own dialect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scalar operands only (Copilot review)
rls setup listed tables and prompted before the service refused
--scope organization with --target-project, and a declined prompt then
exited 0; the conflict is now a usage error before any call (create too).
Comparison and membership operands must be strings, numbers or booleans,
so an object or array is refused even when the live schema is unavailable.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… unique E2E policy table (Copilot review)
str.isdigit() is also true for characters like '²', which int() rejects,
so --target-project ² escaped INVALID_ARGUMENT with a traceback (rls/cls
and semantic-layer scope). Cover MetastoreClient.patch_item directly, and
give the E2E policy a unique table: policies are named by table, so
concurrent runs would collide with a 409.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…xisting helpers
- rls/cls commands share list/detail/schema/create/update/delete and the
printers through a PolicyGroup descriptor; cls.py keeps only its options
- errors go through the existing _handle_service_call (JSON errors keep
details); _is_interactive and the unused project_ids_as_ints are gone
- MetastoreClient builds ALREADY_EXISTS in one place with a caller hint;
the service no longer re-wraps it
- json_utils.draft7_errors replaces the copied Draft7 formatting in flow
and rls validation; one comparison-operator map
- the schema fetch reuses the open metastore client and the project
backend is looked up once per project, not on every write
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… drop backend memo
- exit_code_for() in commands/_helpers.py, used by handle_service_call
(moved there; the semantic layer keeps its alias) and by rls setup, so
a usage error exits 2 there too, as in rls create
- Dialect / PolicyScope enums live in the service layer; the CLI options
and REST bodies import them instead of re-typing the values
- drop the per-service backend memo: under kbagent serve it grew without
bound, keyed by tokens
- drop the REST target/scope validator: the service refuses the
combination and REST answers 400
- remove dead loggers and the validate_policy_structural wrapper, derive
PolicyGroup.label, explicit delete parameters, stale docstrings
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, default, $identity)
Schema 1.1.0 becomes the metastore default (go-monorepo draft PR), so new
writes are 1.1.0 without sending a version.
- rules select identities by principal, principals or IdP groups
- every rule matching one identity applies (RLS conditions OR, CLS columns
union), so the duplicate-principal refusal and the cross-policy listing
it needed are removed
- rls --default (CLI and REST) sets the condition for identities no rule
matches; {"false": true} is a new sentinel (no rows)
- value {"$identity": "email"} / values {"$identity": "groups"} placeholders
are accepted and previewed as <identity.email> / (<identity.groups>)
- a stored policy without dialect (optional in 1.1.0) merges with the
project backend on update; a CLS policy refuses a default
- docs: CLAUDE.md, AGENT_CONTEXT, commands-reference, gotchas, rls-workflow
(new 1.1.0 workflow), keboola-expert
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…irement, vNEXT tags (review)
The engine now combines several policies on one table with AND (CLS:
intersection), so an added policy can only narrow access, and a reader
with no identity is always refused (security review on
keboola/mcp-server#709). The docs said rules combine with OR across
policies. Also: the CLS section no longer mentions the removed
duplicate-principal check, 1.1.0 passages carry (since vNEXT), and the
docs say a 1.1.0 policy needs an MCP engine that reads 1.1.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on main
The rls and cls routers add routes on top of what main already serves, so the operation, path and router
counts in the generated reference change.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ir size limits after the rebase
Main already leaves the SKILL.md description 1 character under the 1024 limit Claude Desktop enforces, and the
expert agent prompt 1055 bytes under its 70000-byte budget, so the rls/cls additions pushed both over once combined
with main (1060 characters, 71063 bytes).
The description now names the feature as RLS/CLS in the covered topics and drops the separate rls/cls trigger
words, with two wording trims that keep their meaning ("large (200 GB) file uploads" is "200 GB file uploads",
"setup and logout" is "setup, logout"). The expert agent's rls/cls table row keeps the command shape, the scope
rules, the 1.1.0 semantics and the pitfalls and points to rls-workflow.md for the rest (default policy,
$identity placeholders, dialect, dry-run, the cls rules example), which already documents them.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…chema 1.1.0)
A rule may select every identified reader with "*" (principal, principals or groups).
The selector must be the only entry of a principals/groups list, and it is refused
locally when the live schema is older than 1.1.0 (in a 1.0.0 object "*" is a literal
that matches nobody). The rule table shows it as "every identified reader", and the
workflow, gotchas and agent docs explain that it opens a table under the deny-all
default of a governed workspace. Tests cover the single-entry rule, the schema gate
and the rendering for both rls and cls.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…er the rebase
Main's lineage notes grew the agent prompt, so the RLS/CLS tool-selection
row pushed it to 70250 bytes against the 70000-byte budget
(tests/test_agent_prompt.py). Shorten the row; the dropped detail
(--default, $identity, setup) stays in rls-workflow.md.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… budget
Main gained a CLI spec gate and a 70000-byte limit for the expert agent prompt
while this branch was open.
- rls setup prompts print through the formatter instead of typer.echo
- handle_service_call keeps the ConfigError subclass code and reports API
errors through emit_api_error
- rls setup is recorded as a terminal-only exception for serve routes
- docs/cli-manifest.json and the spec baseline are regenerated
- the RLS/CLS row of the expert agent prompt is shortened (details stay in
rls-workflow.md) and a few redundant phrases are trimmed
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the kbagent side of RLS v2 (row-level security), companion to
keboola-mcp-serverPR #709, which redesigns RLS around metastore-backedrls-policy/cls-policyobjects (declarative condition primitives instead of hand-written SQL). kbagent only authors the policies; enforcement isquery_datainkeboola-mcp-server.rlscommand group:list,detail,schema,create,update,delete, and a guidedsetupwizard (checkbox table picker + interactive condition builder, interactive-terminal-only).clscommand group (column-level security):list,detail,schema,create,update,delete. Each rule is{principal|principals, visible_columns}-- an allowlist projection.ClsServicesubclassesRlsService; nosetupwizard.targetedby default (the metastore schema's own default: the owning project plus--target-projectgrants).--scope organizationgoverns the table in every project of the organization; it is explicit and destructive-class (rls|cls.create --scope organizationinFLAG_ESCALATIONS, also checked over REST). There is noprojectscope (the schema does not support it).--target-projecttakes an alias or a project ID (the feat(semantic-layer): metastore scope/ACL support (targeted + org-wide) [AI-3790] #715 resolver).targetedpolicies of its own project without grants; grants andorganizationscope need the organization-admin role. Policies are enforced only where the project has therow-level-securityfeature.dialectdiffers from the project backend (--dialectdefaults to the backend), and kbagent refuses that mismatch locally. Anullcomparison (renderscol = NULL, matches nothing) and principals with whitespace are refused too.principal,principalsor IdPgroups; every rule matching one identity applies (RLS conditions OR, CLS columns union), so the old duplicate-principal refusal is gone;rls --default(CLI and REST) sets the condition for an identified reader no rule matches, and{"false": true}means no rows;value {"$identity": "email"}/values {"$identity": "groups"}placeholders are accepted and previewed. A default cannot be removed by update (recreate the policy).updateis a partialPATCH(newMetastoreClient.patch_item) of only the changed keys. The policy is read first and the MERGED result is validated, because the metastore PATCH validates only the keys it receives; grants change before the rules; the result is re-read after the write. The scope support inmetastore_client.py(post_itemscope,put_target_projects) comes from feat(semantic-layer): metastore scope/ACL support (targeted + org-wide) [AI-3790] #715.--dry-runpreview follows the enforcement's rendering (columns quoted per dialect,TRUE/FALSE), but is not the enforcement.create/update/setupareadmin;deleteisdestructive(and has--dry-run)./rlsand/clsrouters built from one builder, every route permission-gated:POST /{group}/{project}(table_id,rules,dialect?,scope,target_projects),PATCH /{group}/{project}/{policy_id},DELETE ...?dry_run=true.--table-id,--dialectchoice,--policy-id,--target-projectalias or ID.CLAUDE.md,context.pyAGENT_CONTEXT,commands-reference.md,gotchas.md,keboola-expert.md,SKILL.md(regenerated), newrls-workflow.md,docs/error-codes.md,docs/web-server-endpoints.md(regenerated).Backend availability: the
rls-policyandcls-policyobject types are registered by the metastore's 2026-09-29 schema migrations (go-monorepo, released asmetastore-v0.11.0). A stack whose metastore predates them answersrls schema/cls schemawith a classifiedNOT_FOUND(seegotchas.md).Known enforcement-side hazard (for #709): an
organizationpolicy is loaded by every project in the organization, whatever its backend, so in a mixed Snowflake/BigQuery organization it blocks every project on the other backend. kbagent can only check the owner's backend; the docs recommendtargetedthere.No version bump, no
changelog.pyentry -- per this repo's binding convention, feature PRs never bump the version;(since vNEXT)placeholders are resolved by the release PR.Test Plan
tests/test_rls_service.py,tests/test_cls_service.py(scope default and combinations, backend dialect, schema 1.1.0 (groups, OR-combined rules, default, $identity), PATCH of changed keys, grants-before-rules, re-read, 409 message, delete dry-run, preview rendering per dialect).tests/test_rls_cli.py,tests/test_cls_cli.py(flags,--scope organizationanddeleteblocked by--deny-destructive,setup --jsonJSON error envelope).tests/test_server_rls.py,tests/test_server_cls.py(PATCH route,target_projects+organization= 422, permission classes per route).tests/test_metastore_client.py,tests/test_semantic_layer_scope.py(target project0rejected).tests/test_e2e.py: authors atargetedpolicy with no grants and the project's own dialect, updates its rules,delete --dry-run, deletes it (not run locally: needsE2E_API_TOKEN).ruff check,ruff format --check,ty check,loc-check,check-sentinel-guards,check_command_sync.py,check_version_gates.py,check_error_codes.py, skill-frontmatter length and theSKILL.md/web-server-endpoints.mdgenerators all pass.Related Issues
Linear: CLI-17
Companion PR: keboola/mcp-server#709 (RFC + PLAN + the enforcement engine). Follow-up: #833 (
semantic-layer edit→ PATCH).🤖 Generated with Claude Code